/*
 * File Name: Presenter.java
 *------------------------------------------------------------------------------
 * Copyright 2012 NCR Corporation
 *------------------------------------------------------------------------------
 * All revision information is updated automatically from source code control
 * change records - please do not manually edit.
 *------------------------------------------------------------------------------
 *
 * $LastChangedRevision:: 1853                                     $
 * $LastChangedBy:: rb250096                                       $
 * $LastChangedDate:: 2012-03-21 23:04:58 -0400 (Wed, 21 Mar 2012) $
 */

package com.ncr.nep.dashboardcore.mvp.client;

import java.util.HashSet;
import java.util.logging.Logger;

import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.inject.Inject;
import com.google.web.bindery.event.shared.Event;
import com.google.web.bindery.event.shared.EventBus;
import com.google.web.bindery.event.shared.HandlerRegistration;
import com.ncr.nep.dashboardcore.utils.client.AppContext;

/**
 * Class defining an MVP presenter object responsible for managing a
 * view.
 *
 * <p>
 *     In addition to providing a reference to the view being managed, the
 *     Presenter class also encapsulates common presenter logic such as
 *     event management via the {@link EventBus} (including automatic
 *     removal of event listeners when the presenter is torn down) and
 *     automatic (optional) setup and tear down of the presenter based on
 *     when the view is attached or detached from the DOM.
 * </p>
 *
 * @author rb250096
 * @version $Revision: 1853 $
 */
public abstract class Presenter<T extends View> implements AttachEvent.Handler {

    //--------------------------------------------------------------------------
    //
    //  Fields
    //
    //--------------------------------------------------------------------------

    /**
     * Logging instance.
     */
    private static Logger log = Logger.getLogger("Presenter");

    /**
     * The view managed by this presenter object.
     */
    protected T view;

    /**
     * Set of {@link HandlerRegistration} objects for all event handlers
     * added through the presenter.
     */
    protected HashSet<HandlerRegistration> eventRegistrations =
            new HashSet<HandlerRegistration>();

    /**
     * Indicates whether this presenter has been set up by invoking the
     * {@link #setup()} method.
     */
    protected boolean isSetup;

    /**
     * Indicates whether this presenter should automatically be setup via
     * its {@link #setup()} method when the view is attached to the DOM.
     */
    protected boolean autoSetup = true;

    /**
     * Event bus used to dispatch application-wide events from the presenter.
     */
    protected EventBus eventBus;

    //--------------------------------------------------------------------------
    //
    //  Constructors
    //
    //--------------------------------------------------------------------------

    /**
     * Constructor
     */
    public Presenter() { }

    /**
     * Constructor
     *
     * @param view The view managed by this presenter object.
     */
    public Presenter(T view) {
        this.setView(view);
    }

    /**
     * Constructor
     *
     * @param view The view managed by this presenter object.
     * @param autoSetup True to have this presenter's {@link #setup()}  method
     *                  automatically called when its {@link View} is attached
     *                  to the DOM.
     */
    public Presenter(T view, boolean autoSetup) {
        this.autoSetup = autoSetup;
        this.setView(view);
    }

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------
    
    /**
     * Gets the view managed with this presenter.
     *
     * @return The view managed with this presenter.
     */
    public T getView() {
        return this.view;
    }

    /**
     * Sets the view managed with this presenter.
     *
     * @param view The view to managed with this presenter.
     */
    @Inject
    public void setView(T view) {
        if(this.view != null && this.isSetup) {
            this.tearDown();
        }

        this.view = view;
        this.wireView();
    }

    /**
     * Gets boolean indicating whether auto setup functionality is enabled
     * for this presenter.
     *
     * @return True if this presenter will automatically have its
     * {@link #setup()} method called when its {@link View} is attached to
     * the DOM.
     */
    public boolean isAutoSetup() {
        return autoSetup;
    }

    /**
     * Sets boolean indicating whether auto setup functionality is enabled
     * for this presenter.
     *
     * @param autoSetup True to have this presenter's {@link #setup()} method
     *                  called automatically when it is attached to the DOM.
     */
    public void setAutoSetup(boolean autoSetup) {
        this.autoSetup = autoSetup;
    }

    /**
     * Gets the {@link EventBus} instance associated with this presenter.
     *
     * @return The EventBus instance for the presenter.
     */
    public EventBus getEventBus() {
        if(this.eventBus != null) {
            return this.eventBus;    
        }
        else {
            EventBus e = AppContext.getEventBus();
            assert e != null: "EventBus not set in AppContext";
            return e;
        }
    }

    /**
     * Sets the {@link EventBus} instance associated with this presenter.
     *
     * @param eventBus The EventBus instance for this presenter.
     */
    public void setEventBus(EventBus eventBus) {
        this.eventBus = eventBus;
    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Wires the {@link View} and this to each other including adding additional
     * {@link AttachEvent.Handler} instances if {@link #autoSetup} is set to
     * <code>true</code>.
     */
    public void wireView() {
        this.view.setPresenter(this);
        if(this.autoSetup) {
            if(this.view.isAttached()) {
                this.setup();
            }
            
            this.eventRegistrations.add(this.view.addAttachHandler(this));
        }
    }

    /**
     * Perform any necessary logic when the presenter and its {@link View} are
     * wired together.  This method can be used for when specific initialization
     * steps need to be performed once the view is available to the presenter.
     */
    public void setup() {
        log.fine("Setting up presenter");
        this.isSetup = true;
        this.view.init();
    }

    /**
     * Perform any necessary logic when the presenter and its {@link View} are
     * no longer in use.  For instance, this method can be called after the view
     * is permanently removed from the DOM.
     */
    public void tearDown() {
        log.fine("Tearing down presenter");

        this.isSetup = false;
        for(HandlerRegistration r : this.eventRegistrations) {
            try {
                r.removeHandler();
            }
            catch(AssertionError ae) {
                //handler was already removed, just continue
            }
        }

        this.view.dispose();
    }

    /**
     * Requests the presenter refresh itself including reload any data and
     * redraw itself.
     *
     * <p>
     *     Subclasses can override this method to perform any necessary logic
     *     in order to redraw their views.
     * </p>
     */
    public void refresh() { }

    /**
     * Fires the specified event to all unfiltered event handlers on the
     * {@link EventBus}.  This method can be used for globally broadcasting
     * application events where listeners are not concerned with the source.
     *
     * @param event The event to fire.
     *
     * @see EventBus
     */
    public void fireEvent(Event<?> event) {
        this.getEventBus().fireEvent(event);
    }

    /**
     * Fires the specified event to all event handlers listening to events
     * from this presenter.  This method can be used when the event should only
     * be dispatched to parties interested in events from this presenter.
     * 
     * @param event The event to fire.
     */
    public void fireSourcedEvent(Event<?> event) {
        this.getEventBus().fireEventFromSource(event, this);
    }

    /**
     * Adds an unfiltered event handler to the {@link EventBus} for the 
     * specified event.
     * 
     * @param type The type of event to listen for.
     * @param handler The handler to be invoked when the event is fired.
     * @param <H> The type of handler.
     *           
     * @return Registration object that can be used to remove the event
     * listener.  By default, calls to the {@link #tearDown()}  method will
     * do this automatically.
     */
    public <H> HandlerRegistration addEventBusHandler(Event.Type<H> type,
                                                      H handler) {
        HandlerRegistration r = this.getEventBus().addHandler(type, handler);
        this.eventRegistrations.add(r);
        return r;
    }

    /**
     * Adds an event handlers to the {@link EventBus} for the specified event
     * type and for the specified event source.
     *
     * @param type The type of event to listen for.
     * @param source The source of the event that is being listened for.
     * @param handler The handler to be invoked when the event is fired.
     * @param <H> The type of handler.
     *
     * @return Registration object that can be used to remove the event
     * listener.  By default, calls to the {@link #tearDown()}  method will
     * do this automatically.
     */
    public <H> HandlerRegistration addEventBusHandlerToSource(
            Event.Type<H> type, Object source, H handler) {
        HandlerRegistration r = this.getEventBus().addHandlerToSource(type, 
                source, handler);
        this.eventRegistrations.add(r);
        return r;
    }

    @Override
    public void onAttachOrDetach(AttachEvent event) {
        if(event.isAttached() && !this.isSetup) {
            this.setup();
        }
        else if(!event.isAttached() && this.isSetup) {
            this.tearDown();
        }
    }
}
